Skip to content

Fixed multiple configNode bugs - #17609

Merged
Caideyipi merged 15 commits into
masterfrom
config-bug
Oct 20, 2026
Merged

Caideyipi merged 15 commits into
masterfrom
config-bug

Conversation

@Caideyipi

@Caideyipi Caideyipi commented May 6, 2026 •

Copy link
Copy Markdown
Collaborator

Description

  1. In Simple mode, a successful response will be returned stating "Metadata has taken effect, but WAL has not been flushed to disk." Configuration changes will be lost after a restart.
  2. In the Simple mode, some files have the same sorting key, posing a risk of log replay in disorder.
  3. The return TSStatus of ConsensusManager.write() is not checked, and the exception is only a warn followed by proceeding to the success branch. This means that if consensus writing is rejected, redirected, or the state machine returns a failure, the caller will still receive ACCEPT, while the local heartbeat cache/metrics have already been created in advance, leading to a divergence between metadata and runtime states.
  4. Simplify getRegionGroupCount() by removing an unused increment of its local counter. This is a cleanup; the returned region count is unchanged.
  5. When taking a snapshot, the regionMaintainTaskList lock is not acquired before traversing. Concurrent offer/poll may throw ConcurrentModificationException or put the maintenance queue snapshot into an inconsistent state.
  6. If the pipe listener fails to process the snapshot, the entire takeSnapshot() method will return false; however, ConfigRegionStateMachine combines and absorbs the failures of loadSnapshot and the pipe listener, which can amplify auxiliary link failures into consensus snapshot failures, and even mask the real failure of snapshot loading.

This PR has:

  • been self-reviewed.
    • concurrent read
    • concurrent write
    • concurrent read and write
  • added documentation for new or modified features or behaviors.
  • added Javadocs for most classes and all non-trivial methods.
  • added or updated version, license, or notice information
  • added comments explaining the "why" and the intent of the code wherever would not be obvious
    for an unfamiliar reader.
  • added unit tests or modified existing tests to cover new code paths, ensuring the threshold
    for code coverage.
  • added integration tests.
  • been tested in a test IoTDB cluster.

Key changed/added classes (or packages if there are too many classes) in this PR

@codecov

codecov Bot commented May 6, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 49.27536% with 140 lines in your changes missing coverage. Please review.
✅ Project coverage is 46.47%. Comparing base (f8135bb) to head (1e1ffe0).
⚠️ Report is 21 commits behind head on master.

Files with missing lines Patch % Lines
...che/iotdb/confignode/manager/node/NodeManager.java 29.60% 88 Missing ⚠️
...nsensus/statemachine/ConfigRegionStateMachine.java 65.95% 48 Missing ⚠️
...onfignode/persistence/partition/PartitionInfo.java 71.42% 2 Missing ⚠️
...de/procedure/impl/node/AddConfigNodeProcedure.java 0.00% 2 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master   #17609      +/-   ##
============================================
+ Coverage     46.03%   46.47%   +0.44%     
- Complexity      712      713       +1     
============================================
  Files          5509     5537      +28     
  Lines        400878   404171    +3293     
  Branches      52375    52795     +420     
============================================
+ Hits         184536   187832    +3296     
+ Misses       216342   216339       -3     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@CRZbulabula CRZbulabula left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall

This PR fixes 6 real correctness bugs in ConfigNode (WAL ordering, log sorting, unchecked consensus writes, off-by-one, concurrent snapshot, pipe-listener failure masking). Changes are well-structured, rollback logic is carefully designed, and test coverage for core scenarios is solid.

Inline comments below cover a few items worth attention before merge.

if (persistStatus.getCode() != TSStatusCode.SUCCESS_STATUS.getStatusCode()) {
return persistStatus;
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WAL replay idempotency requirement

With the new write-ahead ordering (persist → execute), if persistPlanForSimpleConsensus succeeds at line 125 but executor.executeNonQueryPlan fails at line 133, the plan is already persisted in WAL and will be replayed on restart.

This is correct WAL semantics, but it introduces a hard requirement: all ConfigPhysicalPlan implementations must be idempotent under replay. For example, a RegisterDataNodePlan replayed against an already-registered node must not fail or double-register.

Is this idempotency property currently guaranteed across all ConfigPhysicalPlan types? If not, this could cause issues on crash recovery in edge cases.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in cddb4b1. A plan whose execution fails is now removed from the SimpleConsensus WAL by truncating and force-syncing back to the pre-write offset, so normal rollback leaves nothing to replay. ConfigRegionStateMachineTest.testFailedSimpleConsensusWriteRollsBackPersistedPlan verifies the WAL contains no replayable plan. For the registration-related plans touched here, NodeInfoTest.testRegistrationPlansAreIdempotentForWalReplay also executes RegisterDataNodePlan, ApplyConfigNodePlan, and UpdateVersionInfoPlan twice and verifies stable state. If truncation itself fails, the status and operator log explicitly warn that replay remains possible.

ByteBuffer buffer = plan.serializeToByteBuffer();
buffer.position(buffer.limit());
simpleLogWriter.write(buffer);
simpleLogWriter.force();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

force() on every write makes scheduled flush redundant

Now that every write is synchronously forced here, the scheduled flushWALForSimpleConsensus thread (lines 447-455) becomes redundant — it calls simpleLogWriter.force() periodically, but there's nothing left to flush.

This is harmless (double-force is idempotent) but worth either:

  1. Removing the scheduled thread to avoid confusion, or
  2. Adding a brief comment explaining the intentional belt-and-suspenders approach.

For ConfigNode metadata operations (low write frequency), the per-write force() is the right durability choice.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed. The scheduled flush executor and flushWALForSimpleConsensus method were removed; each persisted write is synchronously forced, and the writer is now closed from stop().

"Persist current ConfigPhysicalPlan for ConfigNode SimpleConsensus mode failed", e);
return new TSStatus(TSStatusCode.EXECUTE_STATEMENT_ERROR.getStatusCode())
.setMessage(
"Persist ConfigNode SimpleConsensus log failed: " + String.valueOf(e.getMessage()));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: String.valueOf(e.getMessage()) is redundant — string concatenation with + already handles null by converting to "null". Can simplify to:

"Persist ConfigNode SimpleConsensus log failed: " + e.getMessage()

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in the rewritten persistence error path: the message now directly concatenates e.getMessage().

final List<RegionMaintainTask> copiedRegionMaintainTaskList;
synchronized (regionMaintainTaskList) {
copiedRegionMaintainTaskList = new ArrayList<>(regionMaintainTaskList);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good fix — synchronized copy before iterating. One minor observation: this copies the entire list under the lock. If regionMaintainTaskList can grow very large, this could cause a brief pause. In practice, for ConfigNode this list is typically small, so this is fine. Just noting for future awareness.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for noting this. The snapshot path intentionally copies under the lock to obtain a consistent list, then performs serialization after releasing the lock. As noted, this list is expected to remain small for ConfigNode.

@CRZbulabula CRZbulabula left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PTAL.

}
});
return result.getAndIncrement();
return result.get();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a bug?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this was an off-by-one side effect: getAndIncrement() returned the correct count but unnecessarily advanced the local counter before returning. It now uses get(), and PartitionInfoTest.testRegionGroupCount covers both schema and data region counts.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correction to my earlier reply: result is a local counter, and getAndIncrement() returns its value before incrementing it. Replacing it with get() leaves the returned count unchanged; this removes an unused increment rather than fixing an off-by-one bug. I have corrected item 4 in the PR description accordingly.

Caideyipi added 4 commits May 22, 2026 14:02
# Conflicts:
#	iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/consensus/statemachine/ConfigRegionStateMachine.java
#	iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/node/AddConfigNodeProcedure.java
luoluoyuyu

This comment was marked as outdated.

@luoluoyuyu luoluoyuyu left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review summary

Fixes several ConfigNode correctness issues including WAL persist-before-execute and rollback on execute failure. Before merge, document or test that all ConfigPhysicalPlan types are idempotent under WAL replay when persist succeeds and execute fails.

Also resolve CI failures in this PR.

protected TSStatus write(ConfigPhysicalPlan plan) {
SimpleConsensusPersistResult persistResult = null;
if (ConsensusFactory.SIMPLE_CONSENSUS.equals(CONF.getConfigNodeConsensusProtocolClass())) {
persistResult = persistPlanForSimpleConsensus(plan);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With persist before execute, a successful persistPlanForSimpleConsensus followed by a failed executeNonQueryPlan leaves the plan in WAL and it will be replayed on restart. Every ConfigPhysicalPlan executed on replay must be idempotent. Please add an audit or tests for the plan types touched by this PR.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed. The failed-write test audits the persist-success/execute-failure path and verifies rollback leaves no replayable WAL entry. I also added replay idempotency coverage for the plan types directly touched by the registration changes: RegisterDataNodePlan, ApplyConfigNodePlan, and UpdateVersionInfoPlan.


TSStatus result;
try {
result = executor.executeNonQueryPlan(plan);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rollbackFailedPlanForSimpleConsensus on execute failure is the right safety net. Ensure rollback failure paths are logged clearly enough for operators when WAL and in-memory state may diverge.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed. Rollback failure logging now includes the plan type, WAL file, truncate offset, and pre-write end index. The returned status also explicitly says that the persisted plan may replay after restart, so both operators and callers can see the possible WAL/in-memory divergence.

Caideyipi added 2 commits June 3, 2026 11:30
# Conflicts:
#	iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/path/PartialPath.java
@Caideyipi

Copy link
Copy Markdown
Collaborator Author

Addressed the review feedback and pushed the updates.

Changes:

  • Added WAL rollback/replay coverage: failed SimpleConsensus writes now assert that the rolled-back WAL contains no replayable plan.
  • Added idempotency coverage for replay-relevant NodeInfo plans touched by this PR: RegisterDataNodePlan, ApplyConfigNodePlan, and UpdateVersionInfoPlan.
  • Improved SimpleConsensus rollback failure logging with plan type, log file, truncate offset, and endIndexBeforeWrite; the returned status now also notes that the persisted plan may replay after restart if rollback fails.
  • Removed newly introduced TODO comments; todo-check is now passing.
  • Merged latest master and resolved the PartialPath conflict by keeping master's implementation, so the PR is mergeable again.

Verification:

  • mvn -pl iotdb-core/confignode spotless:apply
  • mvn -pl iotdb-core/node-commons spotless:apply
  • mvn -pl iotdb-core/confignode '-Dtest=ConfigRegionStateMachineTest,NodeInfoTest' test
  • git diff --check origin/master...HEAD

Current CI note: the Simple (17) failures were checkout/network failures on self-hosted runners (gnutls_handshake before tests started). I reran the failed jobs. todo-check has passed; the remaining jobs are still pending.

@sonarqubecloud

sonarqubecloud Bot commented Jun 3, 2026

Copy link
Copy Markdown

# Conflicts:
#	iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/consensus/statemachine/ConfigRegionStateMachine.java
#	iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/persistence/executor/ConfigPlanExecutor.java
@Caideyipi

Copy link
Copy Markdown
Collaborator Author

Verified that the review changes already present in cddb4b1c962 remain intact after the latest master merge, and replied to every inline thread without resolving them.

Current verification:

  • ConfigRegionStateMachineTest: 4 passed
  • NodeInfoTest: 2 passed
  • PartitionInfoTest: 5 passed

The current Windows DataNode UT failure is unrelated to this PR: the runner hit Java heap OOM, followed by a timing-sensitive L1PriorityQueueTest.testPollBlocked failure; 4,420 tests ran and the PR changes do not touch that code. The Sonar Reliability-C issues are also reported in files outside this PR's 11-file diff after the master merge, so there is no additional #17609 code change to apply for those checks.

@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
C Reliability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

@hongzhi-gao

Copy link
Copy Markdown
Contributor
  1. iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/consensus/statemachine/ConfigRegionStateMachine.java:628

    • Trigger: The WAL write completes, but simpleLogWriter.force() throws.
    • Potential issue: The catch returns failure without truncating the appended record or disabling further writes. write() then skips execution. A later successful force can persist that record, and startup replays it despite its never having been applied during normal operation. Subsequent plans can therefore execute against different metadata before and after restart. The captured pre-write offset should also protect persistence-failure paths.
  2. iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/consensus/statemachine/ConfigRegionStateMachine.java:670

    • Trigger: Log rotation successfully closes the writer, then Files.move throws an IOException.
    • Potential issue: closeSimpleLogWriter sets the writer to null before the rename. The exception returns through the persistence failure handler without reopening the original file. Every subsequent write fails the initialization check, so even a transient rename failure permanently prevents metadata writes until restart.

@CRZbulabula CRZbulabula left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Three findings from reviewing the current PR head: WAL persistence failure cleanup, ConfigNode registration rollback side effects, and localization of the new registration errors. Details are attached inline.

.CAN_T_SERIALIZE_CURRENT_CONFIGPHYSICALPLAN_FOR_CONFIGNODE_SIMPLECONSENSUS_MODE,
.PERSIST_CURRENT_CONFIGPHYSICALPLAN_FOR_CONFIGNODE_SIMPLECONSENSUS_MODE_FAILED,
e);
return SimpleConsensusPersistResult.failure(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Remove the appended WAL record when persistence fails

  • If simpleLogWriter.write(buffer) succeeds but force() throws, this catch returns a failure without truncating the record already appended to the WAL.
  • write() then returns before calling the executor, but restart recovery replays the record, so an operation rejected to the caller takes effect after restart. A partial append can also leave an invalid record ahead of subsequent writes.
  • I reproduced the complete-record case with an injected force() failure: status 301, zero executor calls, and a 36-byte WAL record that SingleFileLogReader successfully replays as UpdateVersionInfo.

Please restore and force-sync the saved pre-write offset on persistence failure, and prevent further writes if that cleanup cannot complete. Add coverage for append/force failures as well as executor failures.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 1e1ffe04c8d. Append and force() failures now restore the saved pre-write WAL offset and force-sync the truncation before another write is accepted. If cleanup cannot complete, further SimpleConsensus writes are rejected until restart, with an explicit status and operator log.

Regression tests cover a complete appended record followed by a force failure, a partial append failure, and cleanup failure, in addition to the existing executor-failure test. The replay checks preserve the preceding accepted plan, exclude the failed plan, and verify that a subsequent write succeeds after successful cleanup.

private TSStatus rollbackConfigNodeRegistration(
TConfigNodeLocation configNodeLocation, TSStatus versionUpdateStatus) {
final TSStatus rollbackStatus =
writeConfigPhysicalPlan(new RemoveConfigNodePlan(configNodeLocation));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Avoid decommissioning the joining ConfigNode during registration rollback

  • At this point the joining ConfigNode is already a Raft member: AddConfigNodeProcedure completes ADD_PEER before entering REGISTER_SUCCESS. It therefore also applies this RemoveConfigNodePlan.
  • NodeInfo.removeConfigNode() sets the local node status to Removing when the removed ID is its own. The procedure retries REGISTER_SUCCESS after this exception, but neither applyConfigNode() nor notifyRegisterSuccess() resets that status.
  • A transient version-write failure followed by a successful retry consequently leaves the newly registered ConfigNode rejecting requests in ConfigManager.confirmLeader().

Please use a registration rollback that avoids node-removal lifecycle side effects, or explicitly restore the appropriate node status when registration succeeds. A regression test should cover version-write failure followed by a successful registration retry.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 1e1ffe04c8d. A failed ConfigNode version write no longer submits RemoveConfigNodePlan. Since ADD_PEER has already joined the node to Raft, its membership is retained and the failure is propagated so REGISTER_SUCCESS can retry the idempotent registration and version plans. Heartbeat-cache creation remains after both writes succeed.

The regression test applies the plans through a real NodeInfo as the joining node. It verifies that the node remains Running after the version-write failure and after a successful retry, registration contains one member, the version is updated, and no removal plan is written.

Comment on lines +1434 to +1437
final String failureMessage =
String.format(
"Failed to persist version info for DataNode %d: %s",
dataNodeLocation.getDataNodeId(), describeStatus(versionUpdateStatus));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Localize the new registration failure templates

  • This template, the rollback result messages/logs below, and the analogous AINode/ConfigNode helpers introduce raw English strings in Java code. The new exception/status descriptions and the constants near the top of this class have the same issue.
  • These strings remain English under -P with-zh-locale, because that build only swaps the locale-specific message classes.

Please put each complete user-visible or operator-visible template in both the English and Chinese ConfigNodeMessages classes, preserve its format placeholders, and reference those constants here. This also applies to the new rollback log and exception templates.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 1e1ffe04c8d. The registration failure statuses, rollback logs, exception templates, and fallback status descriptions now use matching English and Chinese ConfigNodeMessages constants. Each outcome uses a complete format template, and the raw English fragment constants were removed. The new WAL failure messages are also localized.

Full-reactor test compilation passed for both locales; all 431 ConfigNode message keys and placeholder counts match. A scan found no added raw English literals in main Java, and all 12 focused WAL/registration tests passed in both locales.

@Caideyipi

Copy link
Copy Markdown
Collaborator Author

@hongzhi-gao Both issues are fixed in 1e1ffe04c8d.

  1. Append/force failure now rolls the WAL back to the saved pre-write offset and force-syncs that truncation. Cleanup failure disables further SimpleConsensus writes until restart. Regression coverage checks complete-record force failures, partial appends, preserved earlier records, and refusal of further writes when cleanup fails.
  2. If rotation closes the writer but the atomic rename fails, it reopens the original log without advancing either index. testRotationRenameFailureReopensOriginalLog reproduces a real rename failure and verifies that the next write succeeds and both accepted plans can be replayed. A failed reopen explicitly disables further writes.

Validation: full-reactor test compilation in English and Chinese; 12 focused tests passed in each locale; ConfigNode Spotless and Checkstyle passed; 431 locale keys and format placeholders match.

@CRZbulabula CRZbulabula left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@Caideyipi
Caideyipi merged commit 08380be into master Oct 20, 2026
38 of 40 checks passed
@Caideyipi
Caideyipi deleted the config-bug branch October 20, 2026 06:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants